#! user/bin/python
# -*- coding: utf-8 -*-

"""
Created on 2021/8/31 17:56

@File: 邮件缓存管理器_messagecache.py
@Author: LT
# -----------------------------------------------------------------------------------------------------
@Description:
为使得ListWindows的实现不变得更复杂，抓门将缓存的逻辑分离并存放在这个文件里。
服务器列表窗口通过创建并内嵌这个类的实例来与邮件服务器互动并追踪已加载的邮件题头和全文。
功能：
管理邮件与题头加载和环境，而非GUI；
MailFetcher类，加上已加载题头和已加载邮件的列表；
调用程序必须处理所需的线程化或GUI接口；

manage message and header loads and context, but not GUI;
a MailFetcher, with a list of already loaded headers and messages;
the caller must handle any required threading or GUI interfaces;

3.0 change: use full message text  Unicode encoding name in local
mailconfig module; decoding happens deep in mailtools, when a message
is fetched - mail text is always Unicode str from that point on;
this may change in a future Python/email: see Chapter 13 for details;

3.0 change: inherits the new mailconfig.fetchlimit feature of mailtools,
which can be used to limit the maximum number of most recent headers or
full mails (if no TOP) fetched on each load request; note that this
feature is independent of the loadfrom used here to limit loads to
newly-arrived mails only, though it is applied at the same time: at
most fetchlimit newly-arrived mails are loaded;

3.0 change: though unlikely, it's not impossible that a user may trigger a
new fetch of a message that is currently being fetched in a thread, simply
by clicking the same message again (msg fetches, but not full index loads,
may overlap with other fetches and sends); this seems to be thread safe here,
but can lead to redundant and possibly parallel downloads of the same mail
which are pointless and seem odd (selecting all mails and pressing View
twice downloads most messages twice!); fixed by keeping track of fetches in
progress in the main GUI thread so that this overlap is no longer possible:
a message being fetched disables any fetch request which it is part of, and
parallel fetches are still allowed as long as their targets do not intersect;
# -----------------------------------------------------------------------------------------------------
"""

from .需要复用的_作者之前写过的各种用例 import 服务器的发送_接收_解析及开发程序包_mailtools as mailtools
from .通用的GUI弹出窗口_popuputil import askPasswordWindow


class MessageInfo:
    """
    邮件缓存列表里的一项
    """
    def __init__(self, hdrtext, size):
        self.hdrtext  = hdrtext            # fulltext是缓存里的邮件
        self.fullsize = size               # hdrtext 仅是题头
        self.fulltext = None               # 如果不能用TOP则fulltext=hdrtext


class MessageCache(mailtools.MailFetcher):
    """
    追踪已加载的题头和邮件；
    从MailFetcher继承服务器传输方法；
    可能在其他应用程序里有用：不假定一定需要GUI或线程；

    原始邮件文本的字节解码成str，以便由email包解析并保存到文件；
    使用本地mailconfig模块里的编码设置；
    抓取后自动用mailtools解码；
    """
    def __init__(self):
        mailtools.MailFetcher.__init__(self)            # 3.0: 继承 fetchEncoding
        self.msglist = []                               # 3.0: 继承 fetchlimit

    def loadHeaders(self, forceReloads, progress=None):
        """
        这里分三种情况来处理：1.最初的全部加载，2.加载新收到的邮件，3.以及删除后的强制重载；

        如果题头列表不变或追加了新项，则不重新抓取查看过的邮件；
        在执行删除后保留缓存中的邮件，除非删除失败；
        2.1：快速检查邮件编号是否仍然同步
        3.0：现在这里的加载也遵守mailconfig.fetchlimit设定的最大值
        """
        if forceReloads:
            loadfrom = 1
            self.msglist = []                         # 邮件编号改变了
        else:
            loadfrom = len(self.msglist)+1            # 从上一次加载末尾处继续

        # 只在加载新收到的邮件时执行
        if loadfrom != 1:
            self.checkSynchError(self.allHdrs())      # 如果已损坏则抛出异常

        # 获取所有或新收到的邮件
        reply = self.downloadAllHeaders(progress, loadfrom)
        headersList, msgSizes, loadedFull = reply

        for (hdrs, size) in zip(headersList, msgSizes):
            newmsg = MessageInfo(hdrs, size)
            if loadedFull:                            # zip结果可能是空的
                newmsg.fulltext = hdrs                # 如果'TOP'不可用，就获取完整邮件
            self.msglist.append(newmsg)

    def getMessage(self, msgnum):                     # 获取原始邮件文本
        cacheobj = self.msglist[msgnum-1]             # 抓取完成后添加到缓存
        if not cacheobj.fulltext:                     # 线程化也没有问题
            fulltext = self.downloadMessage(msgnum)   # 3.0: 代码更简单
            cacheobj.fulltext = fulltext
        return cacheobj.fulltext

    def getMessages(self, msgnums, progress=None):
        """
        在派生线程里预先抓取多封邮件的完整原文；
        2.1：快速检查邮件编号是否仍然同步；
        除非已经加载了索引列表，不然程序走不到这里；
        """
        self.checkSynchError(self.allHdrs())            # 如果已损坏则抛出异常
        nummsgs = len(msgnums)                          # 将邮件加入缓存
        for (ix, msgnum) in enumerate(msgnums):         # 可能已经有一些邮件了
            if progress:
                progress(ix+1, nummsgs)                 # 仅在必要时连接
            self.getMessage(msgnum)                     # 不过可以连接多次

    def getSize(self, msgnum):                          # 封装缓存结构
        return self.msglist[msgnum-1].fullsize          # 它已经变了一次了！

    def isLoaded(self, msgnum):
        return self.msglist[msgnum-1].fulltext

    def allHdrs(self):
        return [msg.hdrtext for msg in self.msglist]

    def deleteMessages(self, msgnums, progress=None):
        """
        如果删除所有邮件编号能够运行，则从邮件缓存移除已删除的条目，但不重载题头列表和已查看的邮件文本：
        缓存列表将反映变更之后的服务器上的邮件编号，如果删除由于某些原因执行失败，
        那么调用程序接下来应当强制重新加载所有题头，因为在某些服务器上，邮件编号可能以无法预测的方式发生了改变；
        2.1：如果邮件服务器支持TOP，那么这个函数现在会通过检查邮件题头来检测邮件编号同步，在派生线程里运行；
        """
        try:
            self.deleteMessagesSafely(msgnums, self.allHdrs(), progress)
        except mailtools.TopNotSupported:
            mailtools.MailFetcher.deleteMessages(self, msgnums, progress)

        # 没有错误，更新索引列表
        indexed = enumerate(self.msglist)
        self.msglist = [msg for (ix, msg) in indexed if ix+1 not in msgnums]


class GuiMessageCache(MessageCache):
    """
    在此添加人格GUI专有的调用，这样就能在没有GUI的应用程序中使用缓存相关的代码了
    """

    def setPopPassword(self, appname):
        """
        在这里的主线程中从GUI获取密码
        在GUI里强制调用，以避免弹出窗口在线程里运行
        """
        if not self.popPassword:
            prompt = 'Password for %s on %s?' % (self.popUser, self.popServer)
            self.popPassword = askPasswordWindow(appname, prompt)

    def askPopPassword(self):
        """
        但这个窗口不要在GUI里弹出：我在派生线程里运行！
        如果试图在同一个线程里弹出，将会导致GUI暂停；
        可以由MailFetcher超类调用，但只有因对话框关闭而导致密码仍然是空字符串时才能这么做。
        """
        return self.popPassword
